/**
 * Copyright (c) 2007-2008 Nearchos Paspallis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * AspectSense
 *
 * Middleware for annotation-based, context-aware and adaptive services for
 * mobile and ubiquitous computing
 *
 * This project is an open-source effort, initiated by Nearchos Paspallis as
 * part of his PhD work at the University of Cyprus (http://www.ucy.ac.cy).
 *
 * More information about the project is available at "http://aspectsense.org".
 */

package org.aspectsense.classloaders;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Enumeration;
import java.util.Set;
import java.util.logging.Logger;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;

/**
 * <p>
 * This {@link ClassLoader} enables the dynamic addition of multiple
 * {@link java.util.jar.JarFile}s, in a way which makes their classes visible
 * to each other.
 * </p>
 * <p>
 * No class files with the same signature are allowed to be loaded in the same
 * {@link ClassLoader}, conforming to the Java ClassLoader standards.
 * </p>
 *
 * <p>
 * Date: Jul 20, 2006<br/>
 * Time: 4:10:51 PM
 * </p>
 *
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 */
public class DefaultJarsClassLoader extends ClassLoader implements JarsClassLoader
{
    // todo make sure that multiple JAR files including files with the same name are handled

    private static final Logger logger = Logger.getLogger(DefaultJarsClassLoader.class.getCanonicalName());

    private HashMap<String,Class> cache = new HashMap<String, Class>();

    private HashSet<JarFile> addedJarFiles = new HashSet<JarFile>();
    private HashMap<String, Long> addedBytecodeSizes = new HashMap<String, Long>();
    private HashMap<String, byte []> addedBytecodes = new HashMap<String, byte[]>();
    private HashMap<String, URL> addedURLs = new HashMap<String, URL>();

    public static JarsClassLoader createDefaultJarsClassLoader()
    {
        return new DefaultJarsClassLoader();
    }

    private DefaultJarsClassLoader()
    {
        super(ClassLoader.getSystemClassLoader());
    }

    private void addEmbeddedJarFile(final byte [] bufferedJarFile, final File rootJarFile)
            throws IOException, JarsClassLoaderException
    {
        final Set<JarEntry> jarEntries = new HashSet<JarEntry>();

        final JarInputStream jarInputStream = new JarInputStream(new ByteArrayInputStream(bufferedJarFile));
        while(true)
        {
            final JarEntry jarEntry = jarInputStream.getNextJarEntry();

            if(jarEntry == null)
            {
                break;
            }

            jarEntries.add(jarEntry);
        }

        // check if any of the included files is already specified
        for(final JarEntry jarEntry : jarEntries)
        {
            if(!jarEntry.isDirectory())
            {
                if(addedBytecodes.containsKey(jarEntry.getName()))
                {
                    throw new JarsClassLoaderException("Some entries in the given jar file are already added to the DefaultJarsClassLoader: " + jarEntry.getName());
                }
            }
        }

        // populates the addedBytecodeSizes map
        for(final JarEntry jarEntry : jarEntries)
        {
            if(!jarEntry.isDirectory())
            {
                addedBytecodeSizes.put(jarEntry.getName(), jarEntry.getSize());
            }
        }

        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bufferedJarFile);
        final JarInputStream zipInputStream = new JarInputStream(byteArrayInputStream);

        ZipEntry zipEntry;
        while((zipEntry = zipInputStream.getNextEntry()) != null)
        {
            if(zipEntry.isDirectory()) continue;

            // handling internal (embedded) JAR libraries
            if(zipEntry.getName().toLowerCase().endsWith(".jar"))
            {
                throw new JarsClassLoaderException("Unsupported 2nd level embedded JAR file!");
            }
            else
            {
// todo
//                    // store in url hashtable
//                    URL zipEntryUrl = new URL("jar", "", file.toURI().toURL() + "!/");
//
//                    addedURLs.put(zipEntry.getName(), zipEntryUrl);

                // store in bytecode hashtable
                long size = zipEntry.getSize();
                // -1 means unknown size.
                if(size == -1) size = addedBytecodeSizes.get(zipEntry.getName());

                byte [] buffer = new byte[(int) size];
                int rb = 0;
                int chunk;
                while (((int) size - rb) > 0)
                {
                    chunk = zipInputStream.read(buffer, rb, (int) size - rb);
                    if (chunk == -1)
                    {
                        break;
                    }
                    rb += chunk;
                }

                // add to internal resource hashtable
                addedBytecodes.put(zipEntry.getName(), buffer);
            }
        }
    }

    /**
     * Populates the {@link #addedBytecodes} and the {@link #addedJarFiles}
     * fields.
     *
     * @param file the JAR file to be added
     * @throws JarsClassLoaderException
     */
    public void addJarFile(final File file)
            throws JarsClassLoaderException
    {
        final JarFile jarFile;

        try
        {
            jarFile = new JarFile(file);
        }
        catch(IOException ioe)
        {
            throw new JarsClassLoaderException("Could not locate jar file", ioe);
        }

        if(addedJarFiles.contains(jarFile))
        {
            throw new JarsClassLoaderException("The given jar file is already added to the DefaultJarsClassLoader: " + jarFile);
        }

        Enumeration<JarEntry> jarEntries;

        // check if any of the included files is already specified
        jarEntries = jarFile.entries();
        while(jarEntries.hasMoreElements())
        {
            JarEntry jarEntry = jarEntries.nextElement();
            if(!jarEntry.isDirectory())
            {
                if(addedBytecodes.containsKey(jarEntry.getName()))
                {
                    throw new JarsClassLoaderException("Some entries in the given jar file are already added to the DefaultJarsClassLoader: " + jarEntry.getName());
                }
            }
        }

        // populates the addedBytecodeSizes map
        jarEntries = jarFile.entries();
        while(jarEntries.hasMoreElements())
        {
            JarEntry jarEntry = jarEntries.nextElement();
            if(!jarEntry.isDirectory())
            {
                addedBytecodeSizes.put(jarEntry.getName(), jarEntry.getSize());
            }
        }

        try
        {
            FileInputStream fileInputStream = new FileInputStream(file);
            BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
            ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream);

            ZipEntry zipEntry;
            while((zipEntry = zipInputStream.getNextEntry()) != null)
            {
                if(zipEntry.isDirectory()) continue;

                // handling inetrnal (embedded) JAR libraries
                if(zipEntry.getName().toLowerCase().endsWith(".jar"))
                {
System.out.println("---adding an embedded JAR library");
                    long size = zipEntry.getSize();
                    // -1 means unknown size.
                    if(size == -1) size = addedBytecodeSizes.get(zipEntry.getName());

                    byte [] buffer = new byte[(int) size];
                    int rb = 0;
                    int chunk;
                    while (((int) size - rb) > 0)
                    {
                        chunk = zipInputStream.read(buffer, rb, (int) size - rb);
                        if (chunk == -1)
                        {
                            break;
                        }
                        rb += chunk;
                    }

                    addEmbeddedJarFile(buffer, file);
                }
                else
                {
                    // store in url hashtable
                    URL zipEntryUrl = new URL("jar", "", file.toURI().toURL() + "!/");

                    addedURLs.put(zipEntry.getName(), zipEntryUrl);

                    // store in bytecode hashtable
                    long size = zipEntry.getSize();
                    // -1 means unknown size.
                    if(size == -1) size = addedBytecodeSizes.get(zipEntry.getName());

                    byte [] buffer = new byte[(int) size];
                    int rb = 0;
                    int chunk;
                    while (((int) size - rb) > 0)
                    {
                        chunk = zipInputStream.read(buffer, rb, (int) size - rb);
                        if (chunk == -1)
                        {
                            break;
                        }
                        rb += chunk;
                    }

                    // add to internal resource hashtable
                    addedBytecodes.put(zipEntry.getName(), buffer);
                }
            }
        }
        catch(FileNotFoundException fnfe)
        {
            throw new JarsClassLoaderException(fnfe);
        }
        catch(IOException ioe)
        {
            throw new JarsClassLoaderException(ioe);
        }

        addedJarFiles.add(jarFile);
    }

    public void removeJarFile(final File file)
            throws JarsClassLoaderException
    {
        //todo
        throw new JarsClassLoaderException("Method not implemented!");
    }

    public Class<?> loadClass(String className) throws
            ClassNotFoundException
    {
        return loadClass(className, true);
    }

    protected synchronized Class<?> loadClass(String className, boolean resolve)
            throws ClassNotFoundException
    {
        Class clazz;

        // first check the cache
        if(cache.containsKey(className)) return cache.get(className);

        // check if this is a system class
        try
        {
            return super.findSystemClass(className);
        }
        catch(ClassNotFoundException cnfe)
        {
            // nothing...
        }

        // try to load class from the Jar file source
        byte [] classBytes = loadClassBytes(className);

        if (classBytes != null)
        {
            // define it (i.e. parse the class file)
            logger.finest("attempting to define class: " + className);

            clazz = defineClass(className, classBytes, 0, classBytes.length);
            if(clazz == null)
            {
                throw new ClassFormatError();
            }

            // store to cache
            cache.put(className, clazz);

            // resolve if necessary
            if(resolve) resolveClass(clazz);

            return clazz;
        }

        // try to resolve the class from the super ClassLoader
        clazz = super.findClass(className);

        // assert clazz != null; otherwise an exception would have been thrown
        return clazz;
    }

    public InputStream getResourceAsStream(String name)
    {
        final InputStream inputStream = super.getResourceAsStream(name);

        if(inputStream != null)
        {
            return inputStream;
        }

        final byte [] buffer = addedBytecodes.get(name);

        return buffer != null ? new BufferedInputStream(new ByteArrayInputStream(buffer)) : null;
    }

    public URL getResource(String name)
    {
        final URL url = super.getResource(name);

        return url != null ? url : findResource(name);
    }

    public Enumeration<URL> getResources(String name)
            throws IOException
    {
        return super.getResources(name);
    }

    protected URL findResource(String name)
    {
        URL url = addedURLs.get(name);

        try
        {
            return new URL(url, name);
        }
        catch(MalformedURLException murle)
        {
            return super.findResource(name);
        }
    }

    private byte [] loadClassBytes(final String className)
    {
        final String formattedClassName = className.replace('.', '/') + ".class";

        return addedBytecodes.get(formattedClassName);
    }
}